// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web.Caching;
using System.Web.Hosting;
using BlueOnionSoftware.Bloget.Properties;

namespace BlueOnionSoftware.Bloget
{
    internal static class Search
    {
        private const string SearchCache = "searchCache";

        // remove all whitespace
        private static readonly Regex _regexWhitespace = new Regex(@"\s+", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        // Remove all html tags
        private static readonly Regex _regexAll = new Regex("<(.|\n)*?>", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        // Stopwords (will give a cleaner result)
        private static readonly Collection<string> _stopWords = GetStopWords();

        // don't search while building
        private static bool _buildingConcordance;

        private class Concordance : KeyedCollection<string, ConcordanceItem>
        {
            protected override string GetKeyForItem(ConcordanceItem item)
            {
                return item.Word;
            }
        }

        private class ConcordanceItem
        {
            private readonly Collection<Post> _posts = new Collection<Post>();

            public string Word { get; set; }

            public Collection<Post> Posts
            {
                get { return _posts; }
            }
        }

        private class Match
        {
            public int Rank { get; set; }

            public Post Post { get; set; }
        }

        private class MatchCollection : KeyedCollection<Post, Match>
        {
            protected override Post GetKeyForItem(Match item)
            {
                return item.Post;
            }
        }

        private static int CompareByRank(Match x, Match y)
        {
            var compare = y.Rank.CompareTo(x.Rank); // reverse order

            if (compare == 0)
            {
                compare = y.Post.Created.CompareTo(x.Post.Created);
            }

            return compare;
        }

        /// <summary>
        /// Returns a array of maxPosts that match the search terms. Posts are ordered from most relevant
        /// to least relevant.
        /// </summary>
        /// <param name="bloget">reference to the hosting bloget control</param>
        /// <param name="query">each word (seperarated by whitespace) is queried (and)</param>
        /// <returns></returns>
        internal static Post[] Find(Bloget bloget, string query)
        {
            var findCache = "find_" + query;

            var posts = HostingEnvironment.Cache[findCache] as Post[];

            if (posts != null)
            {
                return posts;
            }

            var concordance = HostingEnvironment.Cache[SearchCache] as Concordance;

            if (concordance == null && _buildingConcordance == false)
            {
                _buildingConcordance = true;
                ThreadPool.QueueUserWorkItem(BuildConcordance, bloget);
            }

            if (concordance == null || _buildingConcordance)
            {
                return new Post[0];
            }

            var matches = FindMatchingPosts(query, concordance);
            posts = new Post[matches.Length];

            for (var i = posts.Length - 1; i >= 0; --i)
            {
                posts[i] = matches[i].Post;
            }

            HostingEnvironment.Cache.Add(findCache, posts, null, Cache.NoAbsoluteExpiration,
                                         new TimeSpan(0, 1, 0), CacheItemPriority.Default, null);

            return posts;
        }

        private static Match[] FindMatchingPosts(string query, IEnumerable<ConcordanceItem> concordance)
        {
            var matchCollection = new MatchCollection();
            var searchWords = query.ToUpperInvariant().Split();

            foreach (var word in searchWords)
            {
                foreach (var ci in concordance)
                {
                    if (ci.Word.Contains(word))
                    {
                        foreach (var post in ci.Posts)
                        {
                            if (matchCollection.Contains(post))
                            {
                                matchCollection[post].Rank += 1;
                            }

                            else
                            {
                                var match = new Match {Post = post};
                                matchCollection.Add(match);
                            }
                        }
                    }
                }
            }

            var matches = new Match[matchCollection.Count];

            for (var i = 0; i < matches.Length; ++i)
            {
                matches[i] = matchCollection[i];
            }

            Array.Sort(matches, CompareByRank);
            return matches;
        }

        private static void BuildConcordance(object stateInfo)
        {
            const string space = " ";
            const int titleWeightingFactor = 15;
            const int categoryWeightingFactor = 20;
            var bloget = (Bloget) stateInfo;
            var concordance = new Concordance();

            // Build the index by reading every post
            foreach (var post in bloget.Blog.Posts)
            {
                // Add searchable items
                var buildContent = new StringBuilder();
                var title = post.Title.ToUpperInvariant() + space;

                for (var i = 0; i < titleWeightingFactor; i++)
                {
                    buildContent.Append(title);
                }

                foreach (var tag in post.Tags)
                {
                    if (bloget.Blog.Categories.Contains(tag))
                    {
                        var category = bloget.Blog.Categories[tag].Name.ToUpperInvariant() + space;

                        for (var i = 0; i < categoryWeightingFactor; i++)
                        {
                            buildContent.Append(category);
                        }
                    }
                }

                var content = bloget.Blog.LoadContent(post.Id);
                buildContent.Append(content.Text);
                var tempContent = buildContent.ToString();

                // 1. remove all HTML - TODO: remove javascript here...
                tempContent = _regexAll.Replace(tempContent, string.Empty);

                // 2. compress all whitespace to one space
                tempContent = _regexWhitespace.Replace(tempContent, space);

                // 3. ensure no space
                tempContent = tempContent.Trim().ToUpperInvariant();

                // split every wordArg
                var words = tempContent.Split();

                foreach (var word in words)
                {
                    if (_stopWords.Contains(word) == false)
                    {
                        if (concordance.Contains(word))
                        {
                            if (concordance[word].Posts.Contains(post) == false)
                            {
                                concordance[word].Posts.Add(post);
                            }
                        }

                        else
                        {
                            var concordanceItem = new ConcordanceItem {Word = word};
                            concordanceItem.Posts.Add(post);
                            concordance.Add(concordanceItem);
                        }
                    }
                }
            }

            using (var cd = new CacheDependency(HostingEnvironment.MapPath(bloget.DataContext)))
            {
                HostingEnvironment.Cache.Insert(SearchCache, concordance, cd);
            }

            _buildingConcordance = false;
        }

        private static Collection<string> GetStopWords()
        {
            var words = Resources.StopWords.Split();
            var stopWordsCollection = new Collection<string>();

            foreach (var word in words)
            {
                stopWordsCollection.Add(word);
            }

            return stopWordsCollection;
        }
    }
}